package sasp_protocol;

/**
 * 
 */
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.NoSuchPaddingException;
import org.boehn.kmlframework.kml.KmlException;
import java.util.*;
import java.net.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import sasp_messages.MessageHandler;
import sasp_messages.MessageHandler.Commands;
import sasp_messages.MessageHandler.Replies;
import sasp_messages.MessageListener;
import sasp_messages.NEWSupernodeCommand;
import sasp_messages.PRONodeCommand;
import sasp_messages.REQDataCommand;
import sasp_messages.REQDataReply;
import sasp_messages.REQMasterCommand;
import sasp_messages.REQMasterReply;
import sasp_messages.REQMasterTuple;
import sasp_messages.REQTableReply;
import sasp_messages.SASPCommand;
import sasp_messages.SASPReply;
import sasp_messages.UPDDataCommand;
import sasp_KML.KMLgen;
import sasp_messages.CommCipher;
import sasp_messages.NACKReply;
/**
 * @author Simon
 *
 */
public class Protocol implements MessageListener{

private HashSet<Entity> group1 = new HashSet<Entity>();
private Entity self;

private String GroupType;
private String GroupName;

private MessageHandler mh;
private boolean isSupernode;
private boolean isDataXfer;
private int messDelay;
private int numRetries;
private KMLgen K;


public Protocol( MessageHandler _mh) throws IOException  {
		mh = _mh;
                isSupernode = false;
                isDataXfer = false;
                numRetries = 3;
                messDelay = 10;
                
}

public boolean islive(){
    return isDataXfer;
}

public  void setSelfAlias( String _a ) {
    self.SetAlias(_a);
}

public void addKML(KMLgen _K){
    K = _K;
}

private String publicIP;
private String localIP;

/* TRUE == PUBLIC IP, FALSE == LOCAL IP */
public void setIpState( boolean state ) {
    if( state )
    {
        this.self.IP = publicIP;
    }
    else
    {
        this.self.IP = localIP;
    }
}

public void defself(Entity you){
        try {
            /* Retrieve Public Facing IP Address using WHATISMYIP Web Service */
            /* ENABLE PORT FORWARDING!!! */
            self = you;
            java.net.URL URL = new java.net.URL("http://whatismyip.com/automation/n09230945.asp");
            java.net.HttpURLConnection Conn = (HttpURLConnection) URL.openConnection();
            java.io.InputStream InStream = Conn.getInputStream();
            java.io.InputStreamReader Isr = new java.io.InputStreamReader(InStream);
            java.io.BufferedReader Br = new java.io.BufferedReader(Isr);
            //System.out.print("--Your IP address is " + Br.readLine());
            String[] inAddr = mh.getSelfAddress().split(":");
            publicIP = Br.readLine();
            localIP = inAddr[0];
            this.self.IP = publicIP;
            this.self.PORT = Integer.parseInt(inAddr[1]);
        } catch (IOException ex) {
            Logger.getLogger(Protocol.class.getName()).log(Level.SEVERE, null, ex);
        } 
}

/* Return Reference to Group */
public HashSet<Entity> getgroup1(){
	return group1;
}

/* Add New Entity to Group */
public void addEntity( Entity _e ) {
    group1.add(_e);
}

/* Remove Entity from Group */
public void removeEntity( int _id ) {
    Entity e = this.getNodeByID(_id);
    if(e != null) {
        group1.remove(e);
    }
}

/* Check for lowest ID available for assignment */
public int getNextAvailableID( ) {
    int retVal = -1;
    for( int i = 2; i < 128; i++ ) {
        if( getNodeByID(i) == null ) {
            retVal = i;
            break;
        }
    }
  
    return retVal;
}

/* New Supernode Command */
public void ModifyEntity( Entity _e ) {
    Entity originalEntity = this.getNodeByID(_e.GetEntityID());
    if( originalEntity != null ) {
        originalEntity.SetAlias(_e.GetAlias());
        originalEntity.setIpString(_e.getIpString());
    } else {
        group1.add(_e);
    }
}

/* Promote Super Node Table Update */
public void ReplaceEntity( Entity _e ) {
    Entity originalEntity = this.getNodeByID(_e.GetEntityID());
    if( originalEntity != null ) {
        originalEntity.Setlatitude(_e.Getlatitutde());
        originalEntity.Setlongitude(_e.Getlongitude());
        originalEntity.Setaltitude(_e.Getaltitude());
        originalEntity.SetImage(_e.Getimage());
    } 
}

/* Request Data Reply or Update Data Command */
public void MergeEntity( Entity _e ) {
    Entity originalEntity = this.getNodeByID(_e.GetEntityID());
    if( originalEntity != null ) {
        originalEntity.Setlatitude(_e.Getlatitutde());
        originalEntity.Setlongitude(_e.Getlongitude());
        originalEntity.Setaltitude(_e.Getaltitude());
        originalEntity.SetImage(_e.Getimage());
        originalEntity.SetAlias(_e.GetAlias());
    } else {
        group1.add(_e);
    }
}

/* Check if Alias _a is unqiue [TRUE == UNIQUE] */
public boolean checkAlias( String _a ) {
    boolean retVal = true;

    Iterator<Entity> grpIter = group1.iterator();
    while ( grpIter.hasNext() ){
        Entity temp = grpIter.next();
        if( temp.GetAlias().equals(_a) ) {
            retVal = false;
            break;
        }
    }

    return retVal;
}

/* Search Entity HashSet for an Entity with the provided ID */
public Entity getNodeByID( int _i ) {
    Entity retVal = null;
    Entity temp;

    Iterator<Entity> grpIter = group1.iterator();
    while ( grpIter.hasNext() ){
        temp = grpIter.next();
        if( temp.GetEntityID() == _i ) {
            retVal = temp;
            break;
        }
    }

    return retVal;
}

    public void disconnect( ) {
        /* Stop Data Transfer */
        this.isDataXfer = false;

        /* If SuperNode, choose next supernode [lowest entity ID] and Promote -- Notify All Other Nodes of your intent to quit */
        if( isSupernode ) {
            int lowestEnt = 128;
            for( int i = 0; i < group1.size(); i++ ) {
                Entity nextEnt = ((Entity)group1.toArray()[i]);
                if(nextEnt.GetEntityID() < lowestEnt) { lowestEnt = nextEnt.GetEntityID(); }
            }
            
            for(int j = 0; j < group1.size(); j++) {
                Entity nextEnt = ((Entity)group1.toArray()[j]);
                if( nextEnt.GetEntityID() == lowestEnt) {
                    mh.sendProNodeCommand(self.GetEntityID(), nextEnt.GetEntityID(), nextEnt.getMessageCount(), nextEnt.IP, nextEnt.PORT, group1);
                } else {
                    mh.sendQuitCommand(self.GetEntityID(), nextEnt.GetEntityID(), nextEnt.getMessageCount(), nextEnt.IP, nextEnt.PORT);
                }
            }
        } else {
            for( int i = 0; i < group1.size(); i++ ) {
                Entity nextEnt = ((Entity)group1.toArray()[i]);
                mh.sendQuitCommand(self.GetEntityID(), nextEnt.GetEntityID(), nextEnt.getMessageCount(), nextEnt.IP, nextEnt.PORT);
            }

        }
    }

    /* Connect by Sending SuperNode Request to Provided IP/PORT */
public boolean connect( String _ipAddress, int _port ) {
    mh.sendSNRequest(0, 0, _ipAddress, _port, self.getIpString(), self.GetAlias());
    return false;
}

    public void CommandMessageHandler(Commands cmd, SASPCommand mess) {
        /* Make sure that the received command is meant for this entity before processing */
        Entity e = this.getNodeByID(mess.getOriginatorID());
        if( self.GetEntityID() != 0 && mess.getDestinationID() != self.GetEntityID() && e != null) {
            /* Respond to command with NACK -- Wrong Destination ID */
            mh.sendNackReply(self.GetEntityID(), mess.getDestinationID(), mess.getMessageID(), e.IP, e.PORT, "Wrong Destination Entity ID");
            return;
        }

        switch( cmd ) {
            /* New Supernode -- Replace Current Supernode with updated information */
            case NEW_NODE:
                Entity newNode = new Entity();
                newNode.SetEntityID(1);
                newNode.SetAlias(((NEWSupernodeCommand)mess).getAliasString());
                newNode.setIpString(((NEWSupernodeCommand)mess).getIpString());
                this.ModifyEntity(newNode);
                mh.sendAckReply(self.GetEntityID(), mess.getOriginatorID(), mess.getMessageID(), newNode.IP, newNode.PORT);
                break;
            /* Promoted to Supernode -- Modify Self -- Notify All Peers of Status Change */
            case PRO_NODE:
                /* Stop Data Transfer -- Transition Supernode */
                isDataXfer = false;

                Entity replyEnt = this.getNodeByID(mess.getOriginatorID());
                if( replyEnt != null && mess.getOriginatorID() == 1 ) {
                    mh.sendAckReply(self.GetEntityID(), mess.getOriginatorID(), mess.getMessageID(), replyEnt.IP, replyEnt.PORT);

                    PRONodeCommand inCmd = (PRONodeCommand)mess;

                    /* Replace current table with the previous SuperNode's Entity Table */
                    HashSet<Entity> oldGroup = group1;
                    group1 = inCmd.getData();
                    for( int i = 0; i < oldGroup.size(); i++) {
                        this.ReplaceEntity(((Entity)(oldGroup.toArray()[i])));
                    }

                    /* Update Self ID and Notify ALL Nodes in Updated Entity Table */
                    self.SetEntityID(1);
                    isSupernode = true;
                    this.notifyNewNode();

                    isDataXfer = true;
                } else if( replyEnt != null ) {
                    mh.sendNackReply(self.GetEntityID(), mess.getOriginatorID(), mess.getMessageID(), replyEnt.IP, replyEnt.PORT, "Message must originate from SuperNode");
                }
                break;
            case QUIT:
                /* Drop Originator ID from Entity Table -- Left Group No Ack Required*/
                saspmanager.SASPManagerApp.addLine("PRO - Quit Command Received from " + mess.getOriginatorID());
                this.removeEntity(mess.getOriginatorID());
                break;
            case UPD_DATA:
                /* Update Originator ID in Entity Table -- New Location or Other Info */
                saspmanager.SASPManagerApp.addLine("PRO - Ack Reply Generated");
                HashSet<Entity> updMap = ((UPDDataCommand) mess).getData();
                for( int i = 0; i < updMap.size(); i++) {
                    this.MergeEntity((Entity)updMap.toArray()[i]);
                }
                Entity ackEnt = this.getNodeByID(mess.getOriginatorID());
                if( ackEnt != null ) {
                     mh.sendAckReply(self.GetEntityID(), mess.getOriginatorID(), mess.getMessageID(), ackEnt.IP, ackEnt.PORT);
                }
                break;
            case REQ_DATA:
                saspmanager.SASPManagerApp.addLine("PRO - Request Data Reply Generated");
                if( this.getNodeByID(mess.getOriginatorID()) == null ) {
                    Entity newEnt = new Entity();
                    newEnt.setIpString(((REQDataCommand) mess).getIpAddress());
                    newEnt.SetEntityID(mess.getOriginatorID());
                    this.addEntity(newEnt);
                }

                HashSet<Entity> newSet = new HashSet<Entity>();
                newSet.add(self);
                Entity repEnt = this.getNodeByID(mess.getOriginatorID());
                if( repEnt != null ) {
                    mh.sendReqDataReply(self.GetEntityID(), mess.getOriginatorID(), mess.getMessageID(), repEnt.IP, repEnt.PORT, newSet);
                }
                break;
            case REQ_TBL:
                Entity desEnt = this.getNodeByID(mess.getOriginatorID());
                if( isSupernode && desEnt != null ) {
                    saspmanager.SASPManagerApp.addLine("PRO - Request Table Reply Generated");
                    mh.sendReqTableReply(self.GetEntityID(), mess.getOriginatorID(), mess.getMessageID(), desEnt.IP, desEnt.PORT, group1);
                } else if ( desEnt != null ) {
                    mh.sendNackReply(self.GetEntityID(), mess.getOriginatorID(), mess.getMessageID(), desEnt.IP, desEnt.PORT, "Not Supported by Client");
                }
                break;
            case REQ_SN:
                /* Check for SuperNode in Table -- If ID 1 is not in SuperNode, self must be the SuperNode */
                if( this.getNodeByID(1) != null ) {
                    /* Forward Request to Supernode -- Supernode == 1 */
                    Entity sn = this.getNodeByID(1);
                    
                    mh.forwardCommand(sn.IP, sn.PORT, mess);
                } else {
                    /* Initial or Already Supernode */
                    isSupernode = true;
                    self.SetEntityID(1);

                    REQMasterCommand rmcmd = (REQMasterCommand) mess;
                    String [] addr = rmcmd.getIpAddress().split(":");
                    int port = Integer.parseInt(addr[1]);


                    String myIpString = this.self.IP + ":" + Integer.toString(this.self.PORT);

                    /* Make sure Requested Alias is Unique */
                    if( this.checkAlias(rmcmd.getClientAlias()) )
                    {
                        /* Prepare Response with assigned Entity ID */
                        REQMasterTuple rplData = new REQMasterTuple( );
                        rplData.setGroupName("Test");
                        rplData.setGroupType("No-Auth");
                        rplData.setIPAddress(myIpString);
                        rplData.setMessDelay(messDelay);
                        rplData.setNumRetries(numRetries);
                        rplData.setSNAlias(self.GetAlias());

                        int did = this.getNextAvailableID();

                        Entity addEnt = new Entity();
                        addEnt.SetAlias(rmcmd.getClientAlias());
                        addEnt.PORT = port;
                        addEnt.IP = addr[0];
                        addEnt.SetEntityID(did);
                        addEnt.setMessageCount(rmcmd.getMessageID());
                        this.addEntity(addEnt);

                        saspmanager.SASPManagerApp.addLine("PRO - Request Super Node Reply Generated");
                        mh.sendSNReply(1, addEnt.GetEntityID(), rmcmd.getMessageID(), addEnt.IP, addEnt.PORT, rplData);
                        isDataXfer = true;
                    } else {
                        /* Alias already in Table respond to "Unknown" Entity with NACK */
                        mh.sendNackReply(1, 0, rmcmd.getMessageID(), addr[0], port, "User Name Already Used");
                    }
                }
                break;
            default:
                /* Ignore: Handled By Lower Level Message Handler */
                break;
        }
    }

    public void RequestData( ) {
        /* Request Data from all Entitys in the Group */
        for( int i = 0; i < group1.size(); i++ ) {
            Entity nextEnt = ((Entity)group1.toArray()[i]);
            mh.sendReqDataCommand(self.GetEntityID(), nextEnt.GetEntityID(), nextEnt.getMessageCount(), nextEnt.IP, nextEnt.PORT, self.getIpString(), new int[0], new int[0]);
        }
    }

    public void ReplyMessageHandler(Replies rpl, SASPReply mess) {
        /* Verify message is meant for "self" -- throw away reply if Destination ID does not match self */
        if( self.GetEntityID() != 0 && mess.getDestinationID() != self.GetEntityID() ) {
            return;
        }

        switch( rpl ) {
            /* ACK/NACK DO NOTHING IN THIS LAYER -- Used in MessageHandler */
            case NACK:
            case ACK:
                break;
            /* Update Group with Entity Request Data */
            case REQ_DATA:
                HashSet<Entity> updMap = ((REQDataReply) mess).getData();
                for( int i = 0; i < updMap.size(); i++) {
                    this.MergeEntity((Entity)updMap.toArray()[i]);
                }
                break;
            /* Set Group to Table Received from SuperNode */
            case REQ_TBL:
                /* Make sure it came from the SuperNode */
                if( mess.getOriginatorID() == 1 ) {
                    Entity sn = this.getNodeByID(1);
                    HashSet<Entity> newMap = ((REQTableReply) mess).getData();
                    if( sn != null ) {
                        newMap.add(sn);
                    }
                    group1 = newMap;
                    this.RequestData();
                    isDataXfer = true;
                }
                break;
            case REQ_SN:
                /* Set Own Entity ID to Destination ID -- Previously Unassigned or 0 */
                self.SetEntityID(mess.getDestinationID());

                /* Record Group Information */
                REQMasterTuple rmtpl = ((REQMasterReply) mess).getData();
                GroupName = rmtpl.getGroupName();
                GroupType = rmtpl.getGroupType();
                numRetries = rmtpl.getNumRetries();
                messDelay = rmtpl.getMessDelay();

                /* Add Supernode Entity to Group */
                Entity newEnt = new Entity();
                newEnt.setMessageCount(1); /* Reset Message Count */
                newEnt.SetEntityID(mess.getOriginatorID());
                newEnt.SetAlias(rmtpl.getSNAlias());
                String [] splitString = rmtpl.getIPAddress().split(":");
                newEnt.IP = splitString[0];
                newEnt.PORT = Integer.parseInt(splitString[1]);
                this.addEntity(newEnt);


                Entity dest = this.getNodeByID(1);
                mh.sendReqTableCommand(self.GetEntityID(), dest.GetEntityID(), dest.getMessageCount(), dest.IP, dest.PORT);
                break;
            default:
                /* Ignore: Handled by Lower Level Message Handler */
                break;
        }
    }

    public void notifyNewNode( ) {
        saspmanager.SASPManagerApp.addLine("PRO - Notifying Clients of New Supernode");
        for( int i = 0; i < group1.size(); i++ ) {
            Entity nextEnt = ((Entity)group1.toArray()[i]);
            mh.sendNewNodeCommand(self.GetEntityID(), nextEnt.GetEntityID(), nextEnt.getMessageCount(), nextEnt.IP, nextEnt.PORT, self.getIpString(), self.GetAlias());
        }
    }

    public  void UPD_DATA(double[] location)throws IOException, KmlException{
        saspmanager.SASPManagerApp.addLine("PRO - Entity Location Updated");
        self.Setlatitude(location[0]);
        self.Setlongitude(location[1]);
        HashSet<Entity> xmitSet = new HashSet<Entity>();
        xmitSet.add(self);
        /* Send New Data to ALL Entitys in Group */
        for( int i = 0; i < group1.size(); i++ ) {
            Entity nextEnt = ((Entity)group1.toArray()[i]);
            mh.sendUpdDataCommand(self.GetEntityID(), nextEnt.GetEntityID(), nextEnt.getMessageCount(), nextEnt.IP, nextEnt.PORT, xmitSet);
        }
        //K.write();
    }

    public void SuspectDroppedEntity(int ent) {
        this.removeEntity(ent);
    }
}
